深入理解计算机系统这本书大二的时候就买了,因为有很多学长推荐,而且很多计算机名校也用它当<计算机组成原理>的教材.但是由于各种原因一直拖着没读,大三利用寒假读了一下,感觉受益匪浅,但是没有读完,大三下学期断断续续不仅没有继续读,还把前面读了的忘了个差不多,现在准备利用暑假读完,并记一下笔记,以防忘记.
第四章 处理器体系结构
这一章主要是将前面几章的理论用于实践,通过一个相对简单的”Y86”指令集,介绍了机器代码,汇编代码之间的关系,指令流水等概念.
Y86指令比较简单,只实现了一些基本的指令,指令和数长无关,默认立即数都是4个字节,指令是变长的…
- 立即数用小端(little-endian)编码,比如 0x12345 变成4字节应该是 0x00 01 23 45 但是存储到内存里面就是 0x 45 23 01 00
- 数的运算指令会设置条件码 ZF SF 和 OF (零 符号 和 溢出)
- 程序状态码 Stat 代表了程序运行状态(AOK, HLT, ADR, INS),当程序运行不是AOK时,会进入异常处理程序
- 调用一个函数先写的参数会距离栈顶更近,比如一个函数
int sum(int* Start, int Count)
取得传进来的指针Start值用的是 8(%ebp) 取Count值用的是 12(%ebp), 所以,调用的时候, 需要让后面的参数先入栈 - 完整的汇编代码中,开头会指示这段代码的开始地址,结尾会指示这段代码栈的栈尾地址(栈向低地址增长),整个程序就在两者之间运行,所以要保证栈不要增长的太大覆盖了程序代码
pushl %esp
和popl %esp
的选择pushl %esp
: push本来就要减少栈指针esp,而push esp又要将栈指针压栈,最后的结果就有两种情况 :- 压栈的是减少之后的esp
- 压栈的是原始的esp
popl %esp
: 同上,有两种情况 :- esp中存的是原来的esp指向的值
- esp中存的是增减完了的esp指向的值
在Y86中,压入的是原始的esp,弹出的是原始的esp指向的值(具体可以看254页的分段伪代码)
- 存储器和适中:
- 时钟寄存器: 不是%esp这些寄存器,是输入值到输出值由时钟控制的一种硬件
- 随机访问存储器
- 寄存器文件: 里面包括8个程序寄存器(%eax,%esp),对外提供程序寄存器的读写端口
- 虚拟存储器系统:也就是内存
顺序实现Y86
将指令分为六个阶段,每条指令顺序执行,上一条指令执行完最后一个阶段之后才会执行下一个阶段,执行效率低下.
- 取指 : 取指令,根据PC获得icode(指令代码)ifunc(指令功能)寄存器操作码,valC(常数).
- 译码 : 通过寄存器文件及上一步的寄存器操作码得到相应寄存器的值
- 执行 : 执行需要的运算(加减,异或,与),并设置条件码
- 访存 : 访问内存,读或写,比如一些 irmovl , push(栈) 等指令会用到
- 写回 : 将在访存阶段或者执行阶段得到的值写到存储器里面,用到的硬件和译码相同,都是寄存器文件
- 更新PC : 根据指令得到下条指令的地址
两条指令 : call指令运行会将下一条指令的地址压栈,方便返回 ret指令用栈顶的值来更新PC,解释了一个函数运行的经过.
整体执行是用时钟控制的,每次时钟由低到高都执行一个阶段.
寄存器文件有两个写的端口,如果想要对同一个寄存器写的话,只有有限制高的端口会执行写操作,这样做也是为了解决上面说的 pushl %esp
的问题,在指令执行过程中,寄存器文件的两个写端口分别分配给
1. 访存得到的 valM
2. 计算得到的 valE
而pushl %esp
指令刚好这两个都要写esp ,所以需要制定优先级,具体的优先级不同的编码实现不同.
流水线Y86
书中对流水线的解释很到位
…(顾客点餐)通常都会允许多个顾客同时经过系统,而不是要等到一个用户完成了所有从头到尾的过程才让下一个开始.
当某些流水阶段理解晦涩时,可以对比顾客点餐的例子.我是这样理解的: 每个阶段每个时钟都执行不同的指令,比如取值,每个时钟取一条,比如译码,每个时钟译码一条..
使用流水线来处理指令需要流水线寄存器,就是用于每个阶段之间存储的硬件,上面存储的是这个阶段之前,这个阶段需要执行的指令所需的值. 比如访存流水线寄存器(M)里面存储了访存需要的地址,以及上一步运算得到的状态码等等信息.
在276页中的 E寄存器下面可以看到一个叫 Select A
的硬件,它是为了节省存储空间出现的, 在流水线系统中, value A将不止用来存储在寄存器A中得到的值,还用来存储
1. Jxx 不需要跳转时的 valP
2. call 的 valP (需要将本来的下一条指令的地址压栈)
3. irmovl 中本来就需要存的 valA
由于各个指令并不冲突,所以可以共享存储..具体用操作码来区分.
流水线需要解决的一些问题
流水线冒险
两条指令相邻执行,第二条指令执行到译码阶段的时候第一条指令还没执行完,甚至更前面的指令还没执行完,当某条指令的执行需要前面某条指令计算完的值的时候会出现这种问题,有可能会取到错误的值.两种解决方案: 1. 暂停,暂停当前指令等待前面会影响到后面的值的指令执行完(通过插入bubble) 2. 转发,后面的指令用到的值可能在前面的指令中已经计算并存储了下来,只是还没放到寄存器里面,这时可以根据不同的情况直接在前面指令的存储设备里面获得需要的值,而不是在寄存器中取(错误的值).
- 转发有一种情况解决不了,就是两条指令相邻,上一条指令要到访存阶段才能得到正确的值,下一条指令在译码阶段就需要该值,这样就结合暂停的方法,暂停第二个指令,等到第一个指令得到了需要的值再继续执行.
- 另外还有转发的优先级,这个比较容易理解,就是离当前指令越近的指令的指令寄存器优先级越高
异常处理
当某条指令出现异常的时候,后面的指令有可能会更新程序员可见的状态(状态码),必须禁止,比如当访存或者写回阶段出现异常的时候,需要将执行阶段的信号 set_cc
(允许设置状态码)设置为0,就是不允许后续的指令修改状态码.
ret指令
遇到ret指令的时候,需要访存结束(得到返回的地址)才能确定下一条指令的地址,在此期间流水线是一直运行的,需要在ret运行后连续3个周期插入bubble,直到可以得到正确的下一个指令的地址(P297)
预测错误的分支
对于一些分支跳转的语句,Y86会预测始终选择分支,但是这明显不一定是对的,当分支跳转的指令执行到 执行 阶段的时候就可以验证分支跳转的条件是否成立,如果预测正确就能正确执行,如果预测错误,需要”取消”取出来的多余的两条语句,也是通过插入气泡的方式,同时在周期5可以取出正确的下一条语句(P298)
PC选择
在流水系统中,PC选择是第一件事,PC有三种选择 1. 刚才提到的分支预测错误情况下, PC选择M_valA中取出预测错误的指令的 valP 当做下一条,因为预测错误,就要执行本来计算好的 valP (下一条指令)了,至于为什么存在 valA里面,前面有说明.. 2. 刚才提到的ret指令,需要在 W_valM 中获得下一条 3. 其它情况采用预测值 预测策略刚才也提到了,遇到分支的情况,总是预测会跳转,没有分支直接计算下一条.